import { toRaw, createApp } from 'vue'
import { createPinia } from 'pinia'
import { createMemoryHistory, createRouter, createWebHistory } from 'vue-router'

/**
 *
 * @param {*} prefix : String to prefix the unique id
 * @param {*} length : length of the random string
 * @returns A random id string : "prefix<random string of length 'length'>"
 *
 * uniqueId("firstname-", 4) => firstname-XXXX
 */
export const uniqueId = (prefix = '', length = 8) => {
  return (
    prefix +
    Math.ceil(Math.random() * Date.now())
      .toPrecision(length)
      .toString()
      .replace(/[^a-z0-9_-]/gi, '')
  )
}

/**
 * Deep clone an object
 *
 * @param {*} obj : A Js object (Array, Object, ...)
 */
export const clone = (obj) => {
  if (obj === null || typeof obj !== 'object' || 'isActiveClone' in obj)
    return obj

  if (obj instanceof Date) var temp = new obj.constructor()
  else var temp = obj.constructor()

  for (var key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      obj['isActiveClone'] = null
      temp[key] = clone(obj[key])
      delete obj['isActiveClone']
    }
  }
  return temp
}

/**
 * Start a vue application and the pinia store
 *
 * @param {*} MyApplication : Vue application
 * @param {*} tagName : Tag name on which the vue application will be mounted
 * @param {Object} routes : Optional router instance
 */
export const startApp = (MyApplication, tagName = 'vue-app', routes = null) => {
  const app = createApp(MyApplication)
  if (routes) {
    const router = createRouter({
      history: createWebHistory(),
      routes: routes,
    })
    app.use(router)
  }
  // Store management
  const pinia = createPinia()
  app.use(pinia).mount(`#${tagName}`)
  return app
}

/**
 * Format bytes as human-readable text.
 *
 * @param bytes Number of bytes.
 * @param si True to use metric (SI) units, aka powers of 1000. False to use
 *           binary (IEC), aka powers of 1024.
 * @param dp Number of decimal places to display.
 *
 * @return Formatted string.
 */
export function humanFileSize(bytes, si = false, dp = 1) {
  const thresh = si ? 1000 : 1024

  if (Math.abs(bytes) < thresh) {
    return bytes + ' B'
  }

  const units = si
    ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
    : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
  let u = -1
  const r = 10 ** dp

  do {
    bytes /= thresh
    ++u
  } while (
    Math.round(Math.abs(bytes) * r) / r >= thresh &&
    u < units.length - 1
  )

  return bytes.toFixed(dp) + ' ' + units[u]
}

/** Promise-style setTimeout
 * */
function timeout(ms) {
  let _reject
  const p = new Promise((resolve, reject) => {
    setTimeout(resolve, ms)
    _reject = reject
  })
  p.cancel = (err) => _reject(err)
  return p
}

/** Handle concurrent async calls nicely
 *
 * Typical use is HTTP API call for completion as-you type, we want to play nice :
 * - avoid spamming the API (thus, waiting a delay before actually doing the lookup)
 * - when concurrent requests are waiting, discard all of them (promise reject) except the last one.
 *
 * This class tries to handle repeated calls to a lookup function nicely:
 * - triggers the actual call with some delay
 * - if a new query comes before that delay, reject the previous request promise
 *

 * @type {DelayedLookupHandler}
 */
export const DelayedLookupHandler = class {
  /**
   *
   * @param {function} lookupFunction any callable used as lookup function
   * @param {Number} delayBeforeLookup in ms
   */
  constructor(lookupFunction, delayBeforeLookup = 500) {
    this.currentTimer = null
    this.delayBeforeLookup = delayBeforeLookup
    this.lookupFunction = lookupFunction
  }

  async lookup(...args) {
    if (this.currentTimer) {
      this.currentTimer.cancel('Earlier request came, canceling this one')
    }
    this.currentTimer = timeout(this.delayBeforeLookup)
    await this.currentTimer
    this.currentTimer = null
    return this.lookupFunction(...args)
  }
}
